-- Rogue Rage
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
--
-- Darren Grey
-- http://gruesomegames.com

require "engine.class"
require "mod.class.Actor"
require "mod.class.Object"
require "engine.interface.PlayerRest"
require "engine.interface.PlayerRun"
require "engine.interface.PlayerSlide"
require "engine.interface.PlayerMouse"
require "engine.interface.PlayerHotkeys"
require "engine.interface.GameSound"
local Map = require "engine.Map"
local Dialog = require "engine.Dialog"
local ActorTalents = require "engine.interface.ActorTalents"
local DeathDialog = require "mod.dialogs.DeathDialog"
local Astar = require"engine.Astar"
local DirectPath = require"engine.DirectPath"
local Particles = require "engine.Particles"

--- Defines the player
-- It is a normal actor, with some redefined methods to handle user interaction.<br/>
-- It is also able to run and rest and use hotkeys
module(..., package.seeall, class.inherit(
	mod.class.Actor,
	engine.interface.PlayerRest,
	engine.interface.PlayerRun,
	engine.interface.PlayerMouse,
	engine.interface.PlayerSlide,
	engine.interface.PlayerHotkeys
))

function _M:init(t, no_default)
	t.display=t.display or '@'
	t.color_r=255
	t.color_g=50
	t.color_b=20
	t.image = "jammer.png"
	
	--t.energy = {mod = 1}
	
	t.player = true
	t.type = t.type or "player"
	t.faction = t.faction or "players"
	
	t.name = "Jammer"
	
	t.max_life = 100
	t.life = 100
	t.life_regen = 0
	
	t.power = 4
	t.dodge = 1
	t.aim = 1
	t.armour = 1
	t.speed = 0
		
	t.lite = t.lite or 0
	t.sight = 20
	
	t.crit = 0
	t.crit_high = false
	
	t.no_potions = 3
	
	t.no_enemies = 0
	t.wave = 0
	t.score = 0
	
	t.score_this_round = 0
	t.kills_this_round = 0
	
	t.level = 1
	t.score_to_level = 100
	
	mod.class.Actor.init(self, t, no_default)
	-- engine.interface.PlayerHotkeys.init(self, t)

	self.descriptor = {}
end

function _M:move(x, y, force)
	local ox, oy = self.x, self.y
	local moved = mod.class.Actor.move(self, x, y, force)
	
	if game.freeze then game.freeze = false end
	
	if not force and ox == self.x and oy == self.y and self.doPlayerSlide then
		self.doPlayerSlide = nil
		tx, ty = self:tryPlayerSlide(x, y, false)
		if tx then moved = self:move(tx, ty, false) end
	end
	self.doPlayerSlide = nil
	
	if moved then
		
		game.level.map:moveViewSurround(self.x, self.y, 10, 10)
	end
	
	return moved
end

function _M:act()
	if not mod.class.Actor.act(self) then return end
	
	
		
	-- Wave check
	if self.no_enemies < 1 and game.turn % 50 == 0 then self:newWave() end
	
	-- Score update
	if self.score_this_round > 0 then
		local gw, gh = core.display.size()
		game.flyers:add(gw-200, gh-40, 20, (rng.range(0,3)-1) * 0.5, -5, ("+%d"):format(self.score_this_round), {255,255,100})
		self.score = self.score + self.score_this_round
		self.score_to_level = self.score_to_level - self.score_this_round
		self.score_this_round = 0
		self.kills_this_round = 0
	end
	
	-- Level Up
	if self.score_to_level < 1 then self:levelUp() end
	
	-- Funky shader things !
	self:updateMainShader()
	
	-- Resting ? Running ? Otherwise pause
	if not self:restStep() and not self:runStep() and self.player then
		game.paused = true
	end
	
	self.changed = true
end

function _M:newWave()
	self.wave = self.wave + 1
	game.zone.level_range = {1, self.wave}
	game.zone.base_level = self.wave
	for i = 1, 4 do
		local x, y = util.findFreeGrid(7, 6, 6, true, {[Map.ACTOR]=true})
		if x then
			-- Insert particle effect
			local ood=rng.range(0,self.level+3)
			ood = ood - 2
			if ood < 0 then ood = 0 end
			local filter = {max_ood=ood}
			local m = game.zone:makeEntity(game.level, "actor", filter, nil, true)
			game.zone:addEntity(game.level, m, "actor", x, y)
			game.level.map:particleEmitter(x, y, 1, "teleport_in")
			self.no_enemies = self.no_enemies + 1
		end
	end
	
	for i = 1, 4 do
		local x, y = util.findFreeGrid(16, 6, 6, true, {[Map.ACTOR]=true})
		if x then
			-- Insert particle effect
			local ood=rng.range(0,self.level+3)
			ood = ood - 2
			if ood < 0 then ood = 0 end
			local filter = {max_ood=ood}
			local m = game.zone:makeEntity(game.level, "actor", filter, nil, true)
			game.zone:addEntity(game.level, m, "actor", x, y)
			game.level.map:particleEmitter(x, y, 1, "teleport_in")
			self.no_enemies = self.no_enemies + 1
		end
	end
	
	for i = 1, 4 do
		local x, y = util.findFreeGrid(7, 13, 6, true, {[Map.ACTOR]=true})
		if x then
			-- Insert particle effect
			local ood=rng.range(0,self.level+3)
			ood = ood - 2
			if ood < 0 then ood = 0 end
			local filter = {max_ood=ood}
			local m = game.zone:makeEntity(game.level, "actor", filter, nil, true)
			game.zone:addEntity(game.level, m, "actor", x, y)
			game.level.map:particleEmitter(x, y, 1, "teleport_in")
			self.no_enemies = self.no_enemies + 1
		end
	end
	
	for i = 1, 4 do
		local x, y = util.findFreeGrid(16, 13, 6, true, {[Map.ACTOR]=true})
		if x then
			-- Insert particle effect
			local ood=rng.range(0,self.level+3)
			ood = ood - 2
			if ood < 0 then ood = 0 end
			local filter = {max_ood=ood}
			local m = game.zone:makeEntity(game.level, "actor", filter, nil, true)
			game.zone:addEntity(game.level, m, "actor", x, y)
			game.level.map:particleEmitter(x, y, 1, "teleport_in")
			self.no_enemies = self.no_enemies + 1
		end
	end
	
	--extra potion
end

function _M:levelUp()
	local sx, sy = game.level.map:getTileToScreen(self.x, self.y)
	game.flyers:add(sx-10, sy, 20, (rng.range(0,7)-3) * 0.5, -4, "LEVEL UP!", {255,255,20})
	self.level = self.level + 1
	self.score_to_level = self.score_to_level + 75 + self.level*25
	game:playSound("levelup")
	
	local i = rng.range(1,6)
	if i == 1 then
		self.life = self.life + 10
		self.max_life = self.max_life + 10
		game.flyers:add(sx-10, sy+40, 20, (rng.range(0,7)-3) * 0.5, -4, "Life +10!", {255,255,20})
	elseif i == 2 then
		self.power = self.power + 1
		game.flyers:add(sx-10, sy+40, 20, (rng.range(0,7)-3) * 0.5, -4, "Power +1!", {255,255,20})
	elseif i == 3 then
		self.aim = self.aim + 1
		game.flyers:add(sx-10, sy+40, 20, (rng.range(0,7)-3) * 0.5, -4, "Aim +1!", {255,255,20})
	elseif i == 4 then
		self.dodge = self.dodge + 1
		game.flyers:add(sx-10, sy+40, 20, (rng.range(0,7)-3) * 0.5, -4, "Dodge +1!", {255,255,20})
	elseif i == 5 then
		self.armour = self.armour + 1
		game.flyers:add(sx-10, sy+40, 20, (rng.range(0,7)-3) * 0.5, -4, "Armour +1!", {255,255,20})
	elseif i == 6 then
		self.speed = self.speed + 1
		self.energy.mod = 1 + 0.1*self.speed
	end
	
	self.life = self.max_life
end


-- Precompute FOV form, for speed
local fovdist = {}
for i = 0, 30 * 30 do
	fovdist[i] = math.max(1.07 - 0.07*math.sqrt(i), 0.6)
end

function _M:playerFOV()
	-- Clean FOV before computing it
	game.level.map:cleanFOV()
	-- Compute both the normal and the lite FOV, using cache
	self:computeFOV(self.sight or 10, "block_sight", function(x, y, dx, dy, sqdist)
		game.level.map:apply(x, y, fovdist[sqdist])
		game.level.map:updateMap(x, y)
	end, true, false, true)
	self:computeFOV(self.lite, "block_sight", function(x, y, dx, dy, sqdist)
		game.level.map:applyLite(x, y, fovdist[sqdist])
		game.level.map:updateMap(x, y)
	end, true, true, true)
	
	 -- For each entity, generate lite
	local uid, e = next(game.level.entities)
	while uid do
	if e ~= self and e.lite and e.lite > 0 and e.computeFOV then
	e:computeFOV(e.lite, "block_sight", function(x, y, dx, dy, sqdist) game.level.map:applyExtraLite(x, y, fovdist[sqdist]) end, true, true)
	end
	uid, e = next(game.level.entities, uid)
	end
	
end


--- Called before taking a hit, overload mod.class.Actor:onTakeHit() to stop resting and running
function _M:onTakeHit(value, src)
	--self:runStop("taken damage")
	--self:restStop("taken damage")
	local ret = mod.class.Actor.onTakeHit(self, value, src)
	if self.life < (self.max_life*0.3) then
		local sx, sy = game.level.map:getTileToScreen(self.x, self.y)
		game.flyers:add(sx, sy, 30, (rng.range(0,2)-1) * 0.5, 2, "LOW HEALTH!", {255,0,0}, true)
	end
	return ret
end

function _M:die(src)
	if self.game_ender then
		engine.interface.ActorLife.die(self, src)
		game.paused = true
		self.energy.value = game.energy_to_act
		game:registerDialog(DeathDialog.new(self))
	else
		mod.class.Actor.die(self, src)
	end
end

function _M:setName(name)
	self.name = name
	game.save_name = name
end

--- Tries to get a target from the user
function _M:getTarget(typ)
	return game:targetGetForPlayer(typ)
end

--- Sets the current target
function _M:setTarget(target)
	return game:targetSetForPlayer(target)
end

local function spotHostiles(self)
	local seen = false
	-- Check for visible monsters, only see LOS actors, so telepathy wont prevent resting
	core.fov.calc_circle(self.x, self.y, game.level.map.w, game.level.map.h, 20, function(_, x, y) return game.level.map:opaque(x, y) end, function(_, x, y)
		local actor = game.level.map(x, y, game.level.map.ACTOR)
		if actor and self:reactionToward(actor) < 0 and self:canSee(actor) and game.level.map.seens(x, y) then seen = true end
	end, nil)
	return seen
end

--- Can we continue resting ?
-- We can rest if no hostiles are in sight, and if we need life/mana/stamina (and their regen rates allows them to fully regen)
function _M:restCheck()
	if spotHostiles(self) then return false, "hostile spotted" end

	-- Check resources, make sure they CAN go up, otherwise we will never stop
	if self:getPower() < self:getMaxPower() and self.power_regen > 0 then return true end
	if self.life < self.max_life and self.life_regen> 0 then return true end

	return false, "all resources and life at maximum"
end

--- Can we continue running?
-- We can run if no hostiles are in sight, and if we no interesting terrains are next to us
function _M:runCheck()
	--if spotHostiles(self) then return false, "hostile spotted" end

	-- Notice any noticeable terrain
	local noticed = false
	self:runScan(function(x, y)
		-- Only notice interesting terrains
		local grid = game.level.map(x, y, Map.TERRAIN)
		if grid and grid.notice then noticed = "interesting terrain" end
	end)
	if noticed then return false, noticed end

	self:playerFOV()

	return engine.interface.PlayerRun.runCheck(self)
end

--- Move with the mouse
-- We just feed our spotHostile to the interface mouseMove
function _M:mouseMove(tmx, tmy)
	--return engine.interface.PlayerMouse.mouseMove(self, tmx, tmy, spotHostiles)
	return engine.interface.PlayerMouse.mouseMove(self, tmx, tmy)
end


--- Funky shader stuff
function _M:updateMainShader()
	if game.fbo_shader then
		-- Set shader HP warning
		if self.life ~= self.old_life then
			if self.life < (self.max_life*0.3) then game.fbo_shader:setUniform("hp_warning", 1 - (self.life / self.max_life))
			else game.fbo_shader:setUniform("hp_warning", 0) end
		end

		-- Intensify shader
		--[[if self.raging then game.fbo_shader:setUniform("intensify", {1,0,0,1})
		else game.fbo_shader:setUniform("intensify", {0,0,0,0}) -- Disable
		end--]]
		
		-- Blur shader
		--[[if self.raging then game.fbo_shader:setUniform("blur", 1)
		else game.fbo_shader:setUniform("blur", 0) -- Disable
		end--]]

	end
end

function _M:playerPickup()
    -- If 2 or more objects, display a pickup dialog, otherwise just picks up
    if game.level.map:getObject(self.x, self.y, 2) then
        local d d = self:showPickupFloor("Pickup", nil, function(o, item)
            self:pickupFloor(item, true)
            self.changed = true
            d:used()
        end)
    else
        self:pickupFloor(1, true)
        self:sortInven()
        self:useEnergy()
    self.changed = true
    end
end

function _M:playerDrop()
    local inven = self:getInven(self.INVEN_INVEN)
    local d d = self:showInventory("Drop object", inven, nil, function(o, item)
        self:dropFloor(inven, item, true, true)
        self:sortInven(inven)
        self:useEnergy()
        self.changed = true
        return true
    end)
end

function _M:doWear(inven, item, o)
    self:removeObject(inven, item, true)
    local ro = self:wearObject(o, true, true)
    if ro then
        if type(ro) == "table" then self:addObject(inven, ro) end
    elseif not ro then
        self:addObject(inven, o)
    end
    self:sortInven()
    self:useEnergy()
    self.changed = true
end

function _M:doTakeoff(inven, item, o)
    if self:takeoffObject(inven, item) then
        self:addObject(self.INVEN_INVEN, o)
    end
    self:sortInven()
    self:useEnergy()
    self.changed = true
end

function _M:doDrop(inven, item, on_done)
	
	self:dropFloor(inven, item, true, true)
	self:sortInven(inven)
	self:useEnergy()
	self.changed = true
end

function _M:playerUseItem(object, item, inven)

	local use_fct = function(o, inven, item)
	if not o then return end
	local co = coroutine.create(function()
	self.changed = true

	local ret = o:use(self, nil, inven, item) or {}
	if not ret.used then return end

	if ret.destroy then
		if o.multicharge and o.multicharge > 1 then
			o.multicharge = o.multicharge - 1
		else
			local _, del = self:removeObject(self:getInven(inven), item)
			
			self:sortInven(self:getInven(inven))
		end
	
	return true
	end

    self.changed = true
	end)
	local ok, ret = coroutine.resume(co)
	if not ok and ret then print(debug.traceback(co)) error(ret) end
	return true
	end
	
	if object and item then return use_fct(object, inven, item) end
	
	local titleupdator = self:getEncumberTitleUpdator("Use object")
	self:showEquipInven(titleupdator(),
	function(o)
	return o:canUseObject()
	end,
	use_fct
	)
end